<?php
/**
 * This class is used by the Cache class for persistent storage of cached objects using the filesystem.
 *
 * @package System
 * @subpackage Caching
 */
class cacheHandlerFileSystem implements cacheHandler
{
	/**
	 * This is the path to the file which will be used to store the cached item. It is based off of the key.
	 *
	 * @var string
	 */
	protected $path;

	/**
	 * This is the array passed from the main Cache class, which needs to be saved
	 *
	 * @var array
	 */
	protected $data;

	/**
	 * This flag is used to disable the cacheHandler for this one instance.
	 *
	 * @var bool
	 */
	protected $cache_enabled = true;

	/**
	 * This function stores the path information generated by the makePath function so that it does not have to be
	 * calculated each time the handler is called. This only stores path information, it does not store the data to be
	 * cached
	 *
	 * @var array
	 */
	protected static $memStore = array();

	/**
	 * This is the base path for the cache items to be saved in. This defaults to a directory in the tmp directory (as
	 * defined by the connfiguration) called 'cache', which it will create if needed.
	 *
	 * @var string
	 */
	protected static $cachePath;

	/**
	 * This function takes the key and creates the path. If it is unable to create a path using that key, it returns
	 * false.
	 *
	 * @param array $key
	 * @return bool
	 */
	public function setup($key)
	{
		$this->path = self::makePath($key);
		return ($this->path !== false);
	}

	/**
	 * This function retrieves the data from the file. If the file doesn't exist, or is currently being written to, it
	 * will return false. If the file is already being written to, this instance of the handler gets disabled so as not
	 * to have a bunch of writes get queued up when a cache item fails to hit.
	 *
	 * @return bool
	 */
	public function getData()
	{
		if(file_exists($this->path))
		{
			if($data = self::getDataFromFile($this->path))
			{
				return $data;
			}else{
				$this->cache_enabled = false;
				// the only way to get here is if there is a write lock already in place
				// so we disable caching to make sure this one doesn't attempt to write to the file
			}
		}
		return false;
	}

	static function getDataFromFile($path)
	{
		if(file_exists($path))
		{
			include($path);

			if(!isset($data) || !isset($expiration))
				throw new CacheError('Unable to load cache from filesystem');


			return array('data' => $data, 'expiration' => $expiration);
		}
		return false;
	}


	/**
	 * This function takes the data and stores it to the path specified. If the directory leading up to the path does
	 * not exist, it creates it.
	 *
	 * @param array $data
	 * @param int $expiration
	 * @return bool
	 */
	public function storeData($data, $expiration)
	{
		$success = false;
		if(!$this->cache_enabled)
			return false;

		if(!is_dir(dirname($this->path)))
		{
			if(!mkdir(dirname($this->path), 0700, true))
				return false;
		}

		$file = fopen($this->path, 'w+');
		if(flock($file, LOCK_EX))
		{
			// by dumping this behind a php tag and comment, we make it inaccessible should it happen to become web
			// accessible

			switch(Cache::encoding($data))
			{
				case 'bool':
					$dataString = (bool) $data ? 'true' : 'false';
					break;

				case 'serialize':
					$dataString = 'unserialize("' . addslashes(serialize($data)) . '")';
					break;

				case 'none':
				default :
					$dataString = '\'' . addslashes($data) . '\'';
					break;
			}

			$storeString = '<?php ' . PHP_EOL .
			'$expiration = ' . $expiration . ';' . PHP_EOL .
			'$data = ' . $dataString . ';' . PHP_EOL;

			if(!fwrite($file, $storeString))
			{
				$success = false;
			}else{
				$success = true;
			}
			flock($file, LOCK_UN);
			fclose($file);
		}

		return $success;
	}

	/**
	 * This function takes in an array of strings (the key) and uses them to create a path to save the cache item to.
	 * It starts with the cachePath (or a new 'cache' directory in the config temp directory) and then uses each element
	 * of the array as a directory (after putting the element through md5(), which was the most efficient way to make
	 * sure it was filesystem safe). The last element of the array gets a php extension attached to it.
	 *
	 * @param array $key
	 * @return string
	 */
	static protected function makePath($key)
	{
		if(!isset(self::$cachePath))
		{
			$config = Config::getInstance();
			self::$cachePath = $config['path']['temp'] . 'cache/';
		}

		$path = self::$cachePath;

		// When I profiled this compared to the "implode" function, this was much faster
		// This is probably due to the small size of the arrays and the overhead from function calls
		$memkey = '';
		foreach($key as $group)
		{
			$memkey .= $group . '/' ;
		}

		if(isset(self::$memStore['keys'][$memkey]))
		{
			$path = self::$memStore['keys'][$memkey];
		}else{

			foreach($key as $index => $value)
			{
				$key[$index] = md5($value);
			}

			switch (count($key)) {
				case 0:
					return $path;
					break;

				case 1:
					$path .= $key[0] . '.php';
					break;

				default:
					$name = array_pop($key);

					foreach($key as $group)
					{
						$path .= ($group[0]) ? $group . '/' : '';
					}
					$path .= $name . '.php';
					break;
			}
			self::$memStore['keys'][$memkey] = $path;
		}
		return $path;
	}

	/**
	 * This function clears the data from a key. If a key points to both a directory and a file, both are erased. If
	 * passed null, the entire cache directory is removed.
	 *
	 * @param null|array $key
	 * @return bool
	 */
	static public function clear($key = null)
	{
		if(is_null($key))
			$key = '';

		$path = self::makePath($key);

		if($path)
		{

			if(is_file($path))
				unlink($path);

			if(strpos($path, '.php') !== false)
			{
				$dir = dirname($path);
			}elseif(is_dir($path)){
				$dir = $path;
			}

			if($dir)
				deltree($path);

		}else{
			return false;
		}

		return true;
	}

	/**
	 * Cleans out the cache directory by removing all stale cache files and empty directories.
	 *
	 * @return bool
	 */
	static function purge()
	{
		$config = Config::getInstance();

		$filePath = $config['path']['temp'] . 'cache/';

		$directoryIt = new RecursiveDirectoryIterator($filePath);

		foreach(new RecursiveIteratorIterator($directoryIt, RecursiveIteratorIterator::CHILD_FIRST) as $file)
		{
			if($file->isDir())
			{
				$dirFiles = scandir($file->getPathname());
				if($dirFiles && count($dirFiles) == 2)
				{
					rmdir($file->getPathname());
				}

				continue;
			}


			$path = $file->getPathname();

			$data = self::getDataFromFile($path);
			if($data['expiration'] > START_TIME)
				continue;

			unlink($path);
		}


		return true;
	}

	/**
	 * This function checks to see if it is possible to enable this handler. This returns true no matter what, since
	 * this is the handler of last resort.
	 *
	 * @return bool true
	 */
	static function canEnable()
	{
		return true;
	}

}

?>